package com.pig4cloud.pig.auth.support.filter;

/**
 * 验证码过滤器
 * <p>
 * 该过滤器用于在 OAuth2 认证流程中校验验证码。
 * 主要功能：
 * 1. 拦截登录请求，校验图形验证码或短信验证码
 * 2. 支持配置免验证码客户端列表
 * 3. 不同认证模式的验证码策略
 * 4. 验证码存储在 Redis 中，一次性使用
 * 
 * 验证码策略：
 * - 刷新令牌：不需要验证码
 * - 密码模式/客户端模式/授权码模式 + 免验证客户端：不需要验证码
 * - 短信登录模式：必须验证码
 * - 其他情况：需要验证码
 *
 * @author lengleng
 * @date 2025/05/30
 */

import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.exception.ValidateCodeException;
import com.pig4cloud.pig.common.core.util.RedisUtils;
import com.pig4cloud.pig.common.core.util.WebUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Optional;

/**
 * 验证码过滤器：用于处理登录请求中的验证码校验
 *
 * @author lengleng
 * @date 2025/05/30
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ValidateCodeFilter extends OncePerRequestFilter {

	/**
	 * 认证安全配置属性
	 * 包含免验证码客户端列表等配置
	 */
	private final AuthSecurityConfigProperties authSecurityConfigProperties;

	/**
	 * 过滤器内部处理逻辑，用于验证码校验
	 * <p>
	 * 处理流程：
	 * 1. 判断是否为 OAuth2 认证请求
	 * 2. 根据授权类型判断是否需要验证码
	 * 3. 对于需要验证码的请求执行校验
	 * 4. 验证码错误抛出 OAuth2 认证异常
	 * 
	 * @param request HTTP请求
	 * @param response HTTP响应
	 * @param filterChain 过滤器链
	 * @throws ServletException Servlet异常
	 * @throws IOException IO异常
	 */
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		String requestUrl = request.getServletPath();

		// 只处理 OAuth2 令牌端点的请求
		// 其他请求直接放行
		if (!SecurityConstants.OAUTH_TOKEN_URL.equals(requestUrl)) {
			filterChain.doFilter(request, response);
			return;
		}

		// 刷新令牌请求不需要验证码
		// 因为刷新令牌时已经通过了初始认证
		String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
		if (StrUtil.equals(SecurityConstants.REFRESH_TOKEN, grantType)) {
			filterChain.doFilter(request, response);
			return;
		}

		// 检查是否为免验证码客户端
		// 对于密码模式、客户端模式、授权码模式，如果客户端在免验证列表中则跳过验证码
		// 这通常用于内部系统或可信任的客户端应用
		boolean isIgnoreClient = authSecurityConfigProperties.getIgnoreClients().contains(WebUtils.getClientId());
		if (StrUtil.equalsAnyIgnoreCase(grantType, SecurityConstants.PASSWORD, SecurityConstants.CLIENT_CREDENTIALS,
				SecurityConstants.AUTHORIZATION_CODE) && isIgnoreClient) {
			filterChain.doFilter(request, response);
			return;
		}

		// 执行验证码校验
		// 适用场景：
		// 1. 短信验证码登录（必须验证）
		// 2. 非免验证客户端的其他登录方式
		try {
			checkCode();
			filterChain.doFilter(request, response);
		}
		catch (ValidateCodeException validateCodeException) {
			// 将验证码异常转换为 OAuth2 认证异常
			throw new OAuth2AuthenticationException(validateCodeException.getMessage());
		}
	}

	/**
	 * 校验验证码
	 * <p>
	 * 验证码校验逻辑：
	 * 1. 检查验证码参数是否存在
	 * 2. 从 Redis 中获取保存的验证码
	 * 3. 比较用户输入的验证码与保存的验证码
	 * 4. 验证成功后删除 Redis 中的验证码（一次性使用）
	 * 
	 * 支持两种验证码：
	 * - 图形验证码：使用 randomStr 作为缓存键
	 * - 短信验证码：使用手机号作为缓存键
	 * 
	 * @throws ValidateCodeException 验证码为空、不存在或不匹配时抛出
	 */
	private void checkCode() throws ValidateCodeException {
		Optional<HttpServletRequest> request = WebUtils.getRequest();
		// 获取用户输入的验证码
		String code = request.get().getParameter("code");

		if (StrUtil.isBlank(code)) {
			throw new ValidateCodeException("验证码不能为空");
		}

		// 获取验证码标识符
		// 图形验证码使用 randomStr，短信验证码使用手机号
		String randomStr = request.get().getParameter("randomStr");

		// 特殊处理：短信验证码登录时使用手机号作为缓存键
		// 参考 issue: https://gitee.com/log4j/pig/issues/IWA0D
		String mobile = request.get().getParameter("mobile");
		if (StrUtil.isNotBlank(mobile)) {
			randomStr = mobile;
		}

		// 构建 Redis 缓存键
		String key = CacheConstants.DEFAULT_CODE_KEY + randomStr;
		// 检查验证码是否存在
		if (!RedisUtils.hasKey(key)) {
			throw new ValidateCodeException("验证码不合法");
		}

		// 从 Redis 获取保存的验证码
		String saveCode = RedisUtils.get(key);

		// 验证码为空说明已过期或被使用
		if (StrUtil.isBlank(saveCode)) {
			RedisUtils.delete(key);
			throw new ValidateCodeException("验证码不合法");
		}

		// 比较验证码是否匹配
		if (!StrUtil.equals(saveCode, code)) {
			// 验证失败立即删除，防止暴力破解
			RedisUtils.delete(key);
			throw new ValidateCodeException("验证码不合法");
		}
	}

}
